package ar.edu.itba.pod.legajo45129.balance.api;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;

import ar.edu.itba.balance.api.AgentsTransfer;
import ar.edu.itba.balance.api.NodeAgent;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.pod.legajo45129.manager.NodeManager;

public class Balancer {
	private static Logger logger = Logger.getLogger(Balancer.class);

	public static void balance(NodeInformation coordinator) throws RemoteException, NotBoundException {
		Set<NodeInformation> nodes = NodeManager
				.getInstance()
				.getClusterAdministration(coordinator.host(),
						coordinator.port()).connectedNodes();
		List<AgentsOnNode> list = new ArrayList<AgentsOnNode>();
		int totalAgents = 0;
		// calculta total agents and average.
		nodes.add(NodeManager.getInstance().getCoordinator());
		for (NodeInformation n: nodes) {
			int numberOfAgents = NodeManager.getInstance().getAgentsTransfer(n.host(), n.port()).getNumberOfAgents();
			list.add(new AgentsOnNode(n, numberOfAgents));
			totalAgents += numberOfAgents;
		}

		Collections.sort(list);
		int avg = totalAgents/nodes.size();
		int mod = totalAgents % nodes.size();
		logger.debug("avg: " + avg + " | mod: " + mod);
		logger.debug("after sorting: ");
		for (AgentsOnNode aon: list) {
			logger.debug(aon);
		}


		List<NodeAgent> nodeAgents = new ArrayList<NodeAgent>();
		for (AgentsOnNode a: list) {
			logger.trace("aux nodes");
			log(nodeAgents);
			logger.trace("---------------------------");
			AgentsTransfer agentsTransfer = NodeManager.getInstance().getAgentsTransfer(a.getNode().host(), a.getNode().port());
			if (mod > 0) {
				mod--;
				a.setBalanceDif(avg + 1 - a.getAgents());

			} else {
				a.setBalanceDif(avg - a.getAgents());
			}

			if (a.getBalanceDif() < 0) {
				logger.debug("stopAndGet -> " + Math.abs(a.getBalanceDif()) + " agents");
				nodeAgents.addAll(agentsTransfer.stopAndGet(Math.abs(a.getBalanceDif())));
				logger.trace("after stopAndGet -> aux nodes");
				log(nodeAgents);
			} else if (a.getBalanceDif() > 0){
				List<NodeAgent> transfering = new ArrayList<NodeAgent>();
				List<NodeAgent> aux = new ArrayList<NodeAgent>();
				for (NodeAgent na: nodeAgents.subList(0, a.getBalanceDif())) {
					aux.add(na);
				}

				logger.trace("runAgentsOnNode -> sublist nodes");
				log(aux);

				transfering.addAll(aux);
				nodeAgents.removeAll(aux);

				logger.debug("runAgentsOnNode ->" + a.getBalanceDif() + " agents");
				agentsTransfer.runAgentsOnNode(transfering);

				logger.trace("after runAgentsOnNode -> aux nodes");
				log(nodeAgents);
			}

		}

		logger.debug("nodeAgents size: " + nodeAgents.size() + " -> should be zero");
	}

	private static <T> void log(List<T> nodeAgents) {
		for (T o: nodeAgents) {
			logger.trace(o);
		}
		logger.trace("---------------------------");

	}

	/**
	 * Balance algorithm generally used by a node when is shutting down ro called by addAgentToCluster.
	 * @param coordinator
	 * @param agents
	 * @throws RemoteException
	 * @throws NotBoundException
	 */
	public static void balance(NodeInformation coordinator,
			List<NodeAgent> agents, boolean shutdownBalance) throws RemoteException, NotBoundException {
		Set<NodeInformation> nodes = NodeManager
				.getInstance()
				.getClusterAdministration(coordinator.host(),
						coordinator.port()).connectedNodes();
		
		List<NodeAgent> nodeAgents = new ArrayList<NodeAgent>();
		for (NodeAgent na: agents) {
			nodeAgents.add(na);
		}
		
		if (nodes.size() == 0) {
			logger.info("there is not connected nodes to balance agents, running agents on this node");
			NodeManager.getInstance().getAgentsTransfer().runAgentsOnNode(nodeAgents);
			return;
		}
		
		List<AgentsOnNode> list = new ArrayList<AgentsOnNode>();
		int totalAgents = agents.size();;
		// calculate total agents and average.
		
		if (!shutdownBalance) {
			nodes.add(NodeManager.getInstance().getCoordinator());
		}
		for (NodeInformation n: nodes) {
			int numberOfAgents = NodeManager.getInstance().getAgentsTransfer(n.host(), n.port()).getNumberOfAgents();
			list.add(new AgentsOnNode(n, numberOfAgents));
			totalAgents += numberOfAgents;
		}

		Collections.sort(list);
		int avg = (totalAgents)/nodes.size();
		int mod = totalAgents % nodes.size();
		logger.debug("avg: " + avg + " | mod: " + mod);
		logger.debug("after sorting: ");
		for (AgentsOnNode aon: list) {
			logger.debug(aon);
		}
		
		for (AgentsOnNode a: list) {
			logger.trace("aux nodes");
			log(nodeAgents);
			logger.trace("---------------------------");
			AgentsTransfer agentsTransfer = NodeManager.getInstance().getAgentsTransfer(a.getNode().host(), a.getNode().port());
			if (mod > 0) {
				mod--;
				a.setBalanceDif(avg + 1 - a.getAgents());

			} else {
				a.setBalanceDif(avg - a.getAgents());
			}

			if (a.getBalanceDif() < 0) {
				logger.debug("stopAndGet -> " + Math.abs(a.getBalanceDif()) + " agents");
				nodeAgents.addAll(agentsTransfer.stopAndGet(Math.abs(a.getBalanceDif())));
				logger.trace("after stopAndGet -> aux nodes");
				log(nodeAgents);
			} else if (a.getBalanceDif() > 0){
				List<NodeAgent> transfering = new ArrayList<NodeAgent>();
				List<NodeAgent> aux = new ArrayList<NodeAgent>();
				for (NodeAgent na: nodeAgents.subList(0, a.getBalanceDif())) {
					aux.add(na);
				}

				logger.trace("runAgentsOnNode -> sublist nodes");
				log(aux);

				transfering.addAll(aux);
				nodeAgents.removeAll(aux);

				logger.debug("runAgentsOnNode ->" + a.getBalanceDif() + " agents");
				agentsTransfer.runAgentsOnNode(transfering);

				logger.trace("after runAgentsOnNode -> aux nodes");
				log(nodeAgents);
			}

		}

		logger.debug("nodeAgents size: " + nodeAgents.size() + " -> should be zero");
		
	}
}
